package geecache

import (
	"fmt"
	"geecache/consistenthash"
	"geecache/geecachepb"
	"github.com/golang/protobuf/proto"
	"io"
	"log"
	"net/http"
	"net/url"
	"strings"
	"sync"
)

//HTTP 服务端

const (
	defaultBasePath = "/_geecache/"
	defaultReplicas = 50
)

//HTTPPool 既具备了提供 HTTP 服务的能力，也具备了根据具体的 key，创建 HTTP 客户端从远程节点获取缓存值的能力。

// HTTPPool implements PeerPicker for a pool of HTTP peers(同辈人).
type HTTPPool struct {
	self     string //用来记录自己的地址，包括主机名/IP 和端口
	basePath string //basePath 作为节点通讯地址的前缀，默认是 /_geecache/,
	// 那么 http://example.com/_geecache/ 开头的请求，就用于节点间的访问。因为一个主机上还可能承载其他的服务，
	//加一段 Path 是一个好习惯。比如，大部分网站的 API 接口，一般以 /api 作为前缀。

	mu          sync.Mutex
	peers       *consistenthash.Map    //用来根据具体的 key 选择节点
	httpGetters map[string]*httpGetter //映射远程节点与对应的 httpGetter，每一个远程节点对应一个 httpGetter。
}

// Set 实例化一致性哈希算法，并且添加了传入的节点
func (p *HTTPPool) Set(peers ...string) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.peers = consistenthash.New(defaultReplicas, nil)
	p.peers.Add(peers...)
	p.httpGetters = make(map[string]*httpGetter, len(peers))
	for _, peer := range peers {
		p.httpGetters[peer] = &httpGetter{baseURL: peer + p.basePath}
	}
}

// 实现 PeerPicker 接口

// PickPeer 包装一致性哈希算法的 Get() 方法，根据具体的 key，选择节点，返回节点对应的 HTTP 客户端
func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) {
	p.mu.Lock()
	defer p.mu.Unlock()
	if peer := p.peers.Get(key); peer != "" && peer != p.self {
		p.Log("Pick peer %s", peer)
		return p.httpGetters[peer], true
	}
	return nil, false
}

func NewHTTPPool(self string) *HTTPPool {
	return &HTTPPool{
		self:     self,
		basePath: defaultBasePath,
	}
}

func (p *HTTPPool) Log(format string, v ...interface{}) {
	log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...))
}

// ServeHTTP handle all http requests
func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	//判断访问路径的前缀是否是 basePath，不是返回错误
	if !strings.HasPrefix(r.URL.Path, p.basePath) {
		panic("HTTPPool serving unexpected path: " + r.URL.Path)
	}
	p.Log("%s %s", r.Method, r.URL.Path)

	//我们约定的访问路径格式为 /<basepath>/<Groupname>/<key>

	//按照 / 将剩余的字符串切割为 2 部分
	parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2)
	if len(parts) != 2 {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	groupName := parts[0]
	key := parts[1]

	//通过 groupname 获取 group 实例
	group := GetGroup(groupName)
	if group == nil {
		http.Error(w, "no such group: "+groupName, http.StatusNotFound)
		return
	}

	//获取缓存数据
	view, err := group.Get(key)
	//proto.Marshal 编码 HTTP 响应
	body, err := proto.Marshal(&geecachepb.Response{Value: view.ByteSlice()})
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/octet-stream")
	//将缓存值作为 httpResponse 的 body 返回
	w.Write(body)
}

//HTTP 客户端

type httpGetter struct {
	baseURL string //将要访问的远程节点的地址。例如 http://example.com/_geecache/
}

func (h *httpGetter) Get(in *geecachepb.Request, out *geecachepb.Response) error {
	u := fmt.Sprintf(
		"%v %v/%v",
		h.baseURL,
		url.QueryEscape(in.GetGroup()),
		url.QueryEscape(in.GetKey()),
		//QueryEscape 用于对字符串进行 URL 编码，特别是用于对 URL 查询字符串中的特殊字符进行转义，
		//使其符合 URL 规范。这个函数会将字符串中的特殊字符转换成 % 开头的十六进制数字表示，以便在 URL 中正确传输和解析。
	)
	res, err := http.Get(u)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return fmt.Errorf("server returned: %v", res.Status)
	}

	bytes, err := io.ReadAll(res.Body)
	if err != nil {
		return fmt.Errorf("reading response body: %v", err)
	}

	//proto.Unmarshal 解码 HTTP 响应
	if err = proto.Unmarshal(bytes, out); err != nil {
		return fmt.Errorf("decoding response body: %v", err)
	}

	return nil
}
